/*
    Copyright (c) 2006-2007 Ivaylo Ivanov

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
*/
package com.avalonsql.core.db;

import com.avalonsql.core.configs.ConfigIOException;
import com.avalonsql.core.configs.ConnectionConfig;
import com.avalonsql.core.configs.DriverDefinitionConfig;
import com.avalonsql.core.db.connection.ConnectionProperties;
import com.avalonsql.core.db.connection.ConnectionWrapper;
import com.avalonsql.core.db.drivers.DBDriver;
import org.apache.commons.dbcp.BasicDataSource;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Base class that manages all operations with the database
 */
@SuppressWarnings({"StaticNonFinalField", "UtilityClass", "TodoComment", "ChainedMethodCall", "LocalCanBeFinal", "NestedMethodCall", "UnqualifiedStaticUsage", "WeakerAccess", "ProtectedField", "ClassWithoutLogger"})
public class DBManager {

    protected static BasicDataSource dataSource = new BasicDataSource();

    protected static DBManager instance = null;

    protected static ConnectionWrapper currentConnection;

    protected static Map<String, DriverDefinitionConfig> driverDefinitionConfigs = new HashMap<String, DriverDefinitionConfig>();

    public static boolean isConnected() {
        return currentConnection != null;
    }

    public static Map<String, DriverDefinitionConfig> getDriverDefinitions() throws ConfigIOException {
        if (driverDefinitionConfigs.isEmpty()) {
            for (File file : new File(Const.FOLDER_DRIVER_DEFINITIONS).listFiles()) {
                if (file.getName().endsWith(com.avalonsql.core.configs.Const.CONFIG_FILE_EXTENSION)) {
                    driverDefinitionConfigs.put(file.getName(), new DriverDefinitionConfig(Const.FOLDER_DRIVER_DEFINITIONS + file.getName()).reload());
                }
            }
        }
        return driverDefinitionConfigs;
    }

    public static DBDriver loadDBDriver(ConnectionConfig connectionConfig) throws DBException {
        if (connectionConfig == null) {
            throw new IllegalArgumentException("connectionConfig = null");
        }
        try {
            DriverDefinitionConfig driverDefinitionConfig = new DriverDefinitionConfig(
                    Const.FOLDER_DRIVER_DEFINITIONS + connectionConfig.getDriverDefinition()
            ).reload();
            String driverClassName = driverDefinitionConfig.getDriverClassName();
            return (DBDriver) Class.forName(driverClassName).getConstructor(DriverDefinitionConfig.class).newInstance(driverDefinitionConfig);
        } catch (InstantiationException e) {
            throw new DBException(e);
        } catch (IllegalAccessException e) {
            throw new DBException(e);
        } catch (InvocationTargetException e) {
            throw new DBException(e.getCause());
        } catch (NoSuchMethodException e) {
            throw new DBException(e);
        } catch (ClassNotFoundException e) {
            throw new DBException(e);
        } catch (ConfigIOException e) {
            throw new DBException(e);
        }
    }

    public static ConnectionWrapper addConnection(ConnectionConfig connectionConfig) throws DBException {
        return addConnection(connectionConfig, null);
    }

    public static ConnectionWrapper addConnection(ConnectionConfig connectionConfig, DBDriver dbDriver) throws DBException {
        if (connectionConfig == null) {
            throw new IllegalArgumentException("connectionConfig = null");
        }
        if (dbDriver == null) {
            dbDriver = loadDBDriver(connectionConfig);
        }
        ConnectionWrapper connectionWrapper = connect(
                new ConnectionProperties(
                        dbDriver, connectionConfig.getUserName(), connectionConfig.getPassword(), connectionConfig.getConnectionURL()
                )
        );

        String defaultCatalog = connectionConfig.getDefaultCatalog();
        if (!Utils.isEmpty(defaultCatalog)) {
            setCatalog(connectionWrapper, defaultCatalog);

        }

        return connectionWrapper;
    }

    public static ConnectionWrapper connect(ConnectionProperties connectionProperties) throws DBException {
        if (connectionProperties == null) {
            throw new IllegalArgumentException("connectionProperties = null");
        }
        // TODO remove it and make "equals" method of CW to work
        String url = connectionProperties.getUrl();
        try {
            dataSource.close();
            dataSource.setUrl(url);
            dataSource.setUsername(connectionProperties.getUserName());
            dataSource.setPassword(connectionProperties.getPassword());
            Connection connection = dataSource.getConnection(); //DriverManager.getConnection(url, connectionProperties.getUserName(), connectionProperties.getPassword());
            currentConnection = new ConnectionWrapper(connectionProperties, connection);
        } catch (SQLException e) {
            throw new DBException(e);
        }
        return currentConnection;
    }

    public static void closeConnection() throws DBException {
        try {
            currentConnection.getConnection().close();
            currentConnection = null;
        } catch (SQLException e) {
            throw new DBException(e);
        }
    }

    public static void setCatalog(ConnectionWrapper connectionWrapper, String catalog) throws DBException {
        if (connectionWrapper == null) {
            throw new IllegalArgumentException("connectionWrapper = null");
        }
        if (catalog == null) {
            throw new IllegalArgumentException("catalog = null");
        }
        try {
            connectionWrapper.getConnection().setCatalog(catalog);
        } catch (SQLException e) {
            throw new DBException(e);
        }
    }

    public static String getCatalog(ConnectionWrapper connectionWrapper) throws DBException {
        if (connectionWrapper == null) {
            throw new IllegalArgumentException("connectionWrapper = null");
        }
        try {
            return connectionWrapper.getConnection().getCatalog();
        } catch (SQLException e) {
            throw new DBException(e);
        }
    }

    public static ConnectionWrapper getConnection() throws DBException {
        return currentConnection;
    }

    /**
     * @return the array with names of types for current connection
     */
    public static List<String> getTypes(ConnectionWrapper connectionWrapper) throws DBException {
        if (connectionWrapper == null) {
            throw new IllegalArgumentException("connectionWrapper = null");
        }
        String query = connectionWrapper.getConnectionProperties().getDriver().getQuery(Const.QUERY_TYPES);
        if (query == null) {
            return null;
        }

        return executeQuery(connectionWrapper.getConnection(), query).getColumnData(0);
    }

    /**
     * @return the array with names of types for current connection
     */
    public static List<String> getSQLKeywords(ConnectionWrapper connectionWrapper) throws DBException {
        if (connectionWrapper == null) {
            throw new IllegalArgumentException("connectionWrapper = null");
        }
        List<String> result = new ArrayList<String>();
        String query = connectionWrapper.getConnectionProperties().getDriver().getQuery(Const.QUERY_SQL_KEYWORDS);
        if (query != null) {
            result.addAll(executeQuery(connectionWrapper.getConnection(), query).getColumnData(0));
        }
        try {
            for (String keyword : connectionWrapper.getConnection().getMetaData().getSQLKeywords().split(",")) {
                result.add(keyword);
            }
        } catch (SQLException e) {
            throw new DBException(e);
        }

        return result;
    }
    /*
    public static Map<Class<? extends DBObject>, List<DBObject>> getAllHeirs(ConnectionWrapper connectionWrapper, Class<? extends DBObject> parentClass, String... params) throws DBException {
        ConnectionProperties connectionProperties = connectionWrapper.getConnectionProperties();
        DBDriver driver = connectionProperties.getDriver();
        Map<Class<? extends DBObject>, List<DBObject>> result = null;

        result = new HashMap<Class<? extends DBObject>, List<DBObject>>();
        if (parentClass == null) {
            addHeirs(connectionWrapper, result, driver.getRootObjectClass(), params);
        } else {
            List<Class<? extends DBObject>> heirs = driver.getHeirs(parentClass);
            if (heirs != null) {
                for (Class<? extends DBObject> dbObjectClass : heirs) {
                    addHeirs(connectionWrapper, result, dbObjectClass, params);
                }
            }
        }

        return result;
    }

    private static void addHeirs(ConnectionWrapper connectionWrapper, Map<Class<? extends DBObject>, List<DBObject>> result, Class<? extends DBObject> dbObjectClass, String... params)
            throws DBException
    {
        
        List<DBObject> heirs = new ArrayList<DBObject>();
        heirs.addAll(getAllObjects(connectionWrapper, dbObjectClass, params));
        result.put(dbObjectClass, heirs);
    }

  public static List<DBObject> getAllObjects(ConnectionWrapper connectionWrapper, Class<? extends DBObject> className, String... params) throws DBException {
        DBObjectsProvider<DBObject> provider = connectionWrapper.getConnectionProperties().getProvider(className);
        return provider.getByPattern(connectionWrapper, "%", params);
    }
    
    public static void create(ConnectionWrapper connectionWrapper, DBObject dbObject) throws DBException {
        DBObjectsProvider<DBObject> provider = connectionWrapper.getConnectionProperties().getProvider(dbObject.getClass());
        provider.create(connectionWrapper, dbObject);
    }

    public static void delete(ConnectionWrapper connectionWrapper, Class<? extends DBObject> className, String... params) throws DBException {
        DBObjectsProvider<DBObject> provider = connectionWrapper.getConnectionProperties().getProvider(className);
        provider.delete(connectionWrapper, params);
    }

    public static DBObject load(ConnectionWrapper connectionWrapper, Class<? extends DBObject> className, String... params) throws DBException {
        DBObjectsProvider provider = connectionWrapper.getConnectionProperties().getProvider(className);
        return provider.load(connectionWrapper, params);
    }*/

    public static int executeUpdate(String sql) throws DBException {
        if (currentConnection == null) {
            throw new DBException("Not connected to the database");
        }
        return executeUpdate(currentConnection.getConnection(), sql);
    }
    
    public static int executeUpdate(Connection connection, String sql) throws DBException {
        if (connection == null) {
            throw new IllegalArgumentException("connection = null");
        }
        if (sql == null) {
            throw new IllegalArgumentException("sql = null");
        }
        int result;
        PreparedStatement pstmt = null;
        try {
            pstmt = connection.prepareStatement(sql);
            result = pstmt.executeUpdate();
        } catch (SQLException e) {
            throw new DBException(e);
        } finally {
            closeStatement(pstmt);
        }
        return result;
    }

    public static QueryResults executeQuery(String sql) throws DBException {
        if (currentConnection == null) {
            throw new DBException("Not connected to the database");
        }
        return executeQuery(currentConnection.getConnection(), sql);
    }

    public static QueryResults executeQuery(Connection connection, String sql) throws DBException {
        if (connection == null) {
            throw new IllegalArgumentException("connection = null");
        }
        if (sql == null) {
            throw new IllegalArgumentException("sql = null");
        }
        PreparedStatement pstmt = null;
        try {
            pstmt = connection.prepareStatement(sql);
            return executeQuery(pstmt);
        } catch (SQLException e) {
            throw new DBException(e);
        } finally {
            closeStatement(pstmt);
        }
    }

    public static QueryResults executeQuery(Connection connection, String sql, String[] params) throws DBException {
        if (connection == null) {
            throw new IllegalArgumentException("connection = null");
        }
        if (sql == null) {
            throw new IllegalArgumentException("sql = null");
        }
        if (params == null) {
            throw new IllegalArgumentException("params = null");
        }
        PreparedStatement pstmt = null;
        try {
            pstmt = connection.prepareStatement(sql);
            for (int i = 0; i < params.length; i++) {
                if (params[i] == null) {
                    params[i] = "%";
                }
                pstmt.setString(i + 1, params[i]);
            }
            return executeQuery(pstmt);
        } catch (SQLException e) {
            throw new DBException(e);
        } finally {
            closeStatement(pstmt);
        }
    }

    public static QueryResults execute(PreparedStatement pstmt) throws DBException {
        if (pstmt == null) {
            throw new IllegalArgumentException("pstmt = null");
        }
        try {
            if (pstmt.execute()) {
                ResultSet resultSet = pstmt.getResultSet();
                try {
                    return extractQueryResults(resultSet);
                } finally {
                    closeResultSet(resultSet);
                }
            } else {
                return new QueryResults(pstmt.getUpdateCount());
            }
        } catch (SQLException e) {
            throw new DBException(e);
        }
    }

    public static QueryResults executeQuery(PreparedStatement pstmt) throws DBException {
        if (pstmt == null) {
            throw new IllegalArgumentException("pstmt = null");
        }
        ResultSet rs = null;
        try {
            rs = pstmt.executeQuery();
            return extractQueryResults(rs);
        } catch (SQLException e) {
            throw new DBException(e);
        } finally {
            closeResultSet(rs);
        }
    }

    public static void closeConnection(Connection connection) {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
            }
        }
    }

    public static void closeResultSet(ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
            }
        }
    }

    public static void closeStatement(Statement stmt) {
        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException e) {
            }
        }
    }

    public static QueryResults extractQueryResults(ResultSet resultSet) throws DBException {
        if (resultSet == null) {
            throw new IllegalArgumentException("resultSet = null");
        }
        List<String[]> data = new ArrayList<String[]>();
        try {

            ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
            int columnsCount = resultSetMetaData.getColumnCount();

            QueryResultMetaData[] queryResultMetaData = new QueryResultMetaData[columnsCount];
            for (int i = 0; i < queryResultMetaData.length; i++) {
                queryResultMetaData[i] = new QueryResultMetaData(
                        resultSetMetaData.getColumnName(i + 1),
                        resultSetMetaData.getColumnType(i + 1),
                        resultSetMetaData.getColumnTypeName(i + 1)
                );
            }

            while (resultSet.next()) {
                String[] row = new String[columnsCount];
                for (int i = 0; i < columnsCount; i++) {
                    if (Types.OTHER > queryResultMetaData[i].getType()) {
                        row[i] = resultSet.getString(i + 1);
                    } else {
                        row[i] = queryResultMetaData[i].getTypeName();
                    }
                }
                data.add(row);
            }
            return new QueryResults(data, queryResultMetaData);
        } catch (SQLException e) {
            throw new DBException(e);
        }
    }

}
